package com.example.handler.impl;

import com.example.constant.Attribute;
import com.example.constant.QualifiedName;
import com.example.util.CustomNotifier;
import com.example.util.PsiElementUtil;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import org.apache.commons.lang3.StringUtils;

import java.util.Objects;

/**
 * @author Aaron
 * @since 2020/11/8 16:21
 * <p>描述：</p>
 */
public class FeignMethodGoToImplHandler extends BaseGoToImplHandler {

    private final AnActionEvent anActionEvent;
    private final PsiElement currentPsiElement;

    public FeignMethodGoToImplHandler(AnActionEvent anActionEvent, PsiElement currentPsiElement) {
        this.anActionEvent = anActionEvent;
        this.currentPsiElement = currentPsiElement;
    }

    @Override
    public void doHandle() {
        super.doHandle();

        // 当光标在方法上
        PsiMethod psiMethod = (PsiMethod) currentPsiElement;
        CustomNotifier.info(anActionEvent.getProject(), "Going to " + psiMethod.getName() + "().");

        String url = PsiElementUtil.getReferenceUrl(psiMethod, QualifiedName.Annotation.FEIGN_CLIENT, Attribute.FeignClient.PATH);

        // 获取 @FeignClient 注解的 value/name 属性，即接口的其它模块名
        String valueInFeignClient = PsiElementUtil.getAttributeValue(psiMethod,
                QualifiedName.Annotation.FEIGN_CLIENT, Attribute.FeignClient.VALUE);
        String nameInFeignClient = PsiElementUtil.getAttributeValue(psiMethod,
                QualifiedName.Annotation.FEIGN_CLIENT, Attribute.FeignClient.NAME);
        String moduleName = StringUtils.isBlank(nameInFeignClient) ? valueInFeignClient : nameInFeignClient;

        // 获取当前 Project
        Project project = anActionEvent.getProject();
        if (project == null) {
            System.out.println("Can't get the project by the action event.");
            return;
        }

        VirtualFile[] contentRoots = ProjectRootManager.getInstance(anActionEvent.getProject()).getContentRoots();
        // 按照约定，Module 的名称应该和 @FeignClient 的 name/value 属性值相同，因此当我们查找到该模块，则优先查找该目录下的文件
        for (VirtualFile contentRoot : contentRoots) {
            if (!contentRoot.isDirectory()) {
                continue;
            }

            PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(contentRoot);
            if (null != psiDirectory && Objects.equals(psiDirectory.getName(), moduleName)) {
                boolean isSuccess = this.doGoToImpl(project, psiDirectory, url);
                if (isSuccess) {
                    return;
                }
                break;
            }
        }

        for (VirtualFile virtualFile : contentRoots) {
            PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(virtualFile);
            if (null == psiDirectory || Objects.equals(psiDirectory.getName(), moduleName)) {
                continue;
            }

            boolean isSuccess = this.doGoToImpl(project, psiDirectory, url);
            if (isSuccess) {
                return;
            }
        }

        CustomNotifier.error(anActionEvent.getProject(), "No target was found.");
    }


    /**
     * 跳转实现
     *
     * @param project      工程
     * @param psiDirectory pom.xml 文件 的父目录
     * @param url          RequestMapping 的地址
     * @return 是否已跳转到实现
     */
    private boolean doGoToImpl(Project project, PsiDirectory psiDirectory, String url) {
        // 由于 PsiDirectory 的目录和文件需要分开获取，所以这里要分开处理
        // 获取该目录的子目录，递归处理
        PsiDirectory[] subdirectories = psiDirectory.getSubdirectories();
        for (PsiDirectory subdirectory : subdirectories) {
            if (this.doGoToImpl(project, subdirectory, url)) {
                return true;
            }
        }

        // 获取该目录下的文件
        PsiFile[] files = psiDirectory.getFiles();
        // 遍历该目录下的所有文件
        for (PsiFile file : files) {
            // 不是 Java 类型的文件直接跳过
            if (!(file.getFileType() instanceof JavaFileType)) {
                continue;
            }

            // 文件名中没有含有 Controller 的直接跳过
            if (!file.getName().contains("Controller")) {
                continue;
            }

            if (this.doGoToFile(project, file, url)) {
                return true;
            }

        }


        return false;
    }

    private boolean doGoToFile(Project project, PsiFile file, String url) {
        // 获取该文件的子 Psi 元素
        PsiElement[] children = file.getChildren();
        for (PsiElement child : children) {
            if (!(child instanceof PsiClass)) {
                // 这里只处理类型为 PsiClass 的元素，除此之外还有 PsiPackage、PsiWhiteSpace、PsiImportList 等
                continue;
            }

            PsiAnnotation restController = PsiElementUtil.getAnnotationAtClass(child, QualifiedName.Annotation.REST_CONTROLLER);
            PsiAnnotation controller = PsiElementUtil.getAnnotationAtClass(child, QualifiedName.Annotation.CONTROLLER);
            if (restController == null && controller == null) {
                // Feign 接口的控制器实现至少要包含 @Controller 或 @RestController 中的一个
                continue;
            }

            // 获取 Feign 接口实现的 Url
            String classRequestMapping = PsiElementUtil.getAttributeValueOfAnnotationAtClass(child,
                    QualifiedName.Annotation.REQUEST_MAPPING, Attribute.RequestMapping.VALUE);
            classRequestMapping = StringUtils.isBlank(classRequestMapping) ? "" : classRequestMapping;
            for (PsiElement psiElement : child.getChildren()) {
                if (!(psiElement instanceof PsiMethod)) {
                    continue;
                }

                // 获取 @RequestMapping 注解的 value/name 属性，即接口的 Url 地址
                String valueInRequestMapping = PsiElementUtil.getAttributeValueOfAnnotationAtMethod(psiElement,
                        QualifiedName.Annotation.REQUEST_MAPPING, Attribute.RequestMapping.VALUE);
                String nameInRequestMapping = PsiElementUtil.getAttributeValueOfAnnotationAtMethod(psiElement,
                        QualifiedName.Annotation.REQUEST_MAPPING, Attribute.RequestMapping.NAME);
                String requestMapping = (StringUtils.isBlank(valueInRequestMapping) ? "" : valueInRequestMapping)
                        + (StringUtils.isBlank(nameInRequestMapping) ? "" : nameInRequestMapping);

                // 获取 @GetMapping 注解的 value/name 属性，即接口的 Url 地址
                String valueInGetMapping = PsiElementUtil.getAttributeValueOfAnnotationAtMethod(psiElement,
                        QualifiedName.Annotation.GET_MAPPING, Attribute.RequestMapping.VALUE);
                String nameInGetMapping = PsiElementUtil.getAttributeValueOfAnnotationAtMethod(psiElement,
                        QualifiedName.Annotation.GET_MAPPING, Attribute.RequestMapping.NAME);
                String getMapping = (StringUtils.isBlank(valueInGetMapping) ? "" : valueInGetMapping)
                        + (StringUtils.isBlank(nameInGetMapping) ? "" : nameInGetMapping);

                // 获取 @PostMapping 注解的 value/name 属性，即接口的 Url 地址
                String valueInPostMapping = PsiElementUtil.getAttributeValueOfAnnotationAtMethod(psiElement,
                        QualifiedName.Annotation.POST_MAPPING, Attribute.RequestMapping.VALUE);
                String nameInPostMapping = PsiElementUtil.getAttributeValueOfAnnotationAtMethod(psiElement,
                        QualifiedName.Annotation.POST_MAPPING, Attribute.RequestMapping.NAME);
                String postMapping = (StringUtils.isBlank(valueInPostMapping) ? "" : valueInPostMapping)
                        + (StringUtils.isBlank(nameInPostMapping) ? "" : nameInPostMapping);
                String mapping = requestMapping + getMapping + postMapping;
                if (Objects.equals(classRequestMapping + mapping, url)
                        || Objects.equals((classRequestMapping + mapping).replaceAll("/", ""),
                        url.replaceAll("/", ""))) {
                    // Url 完全相同，或者替换掉分隔符 / 后相同
                    return doLocate(project, mapping, file);
                }
            }
        }

        return false;
    }


}
